/*
 * JANN - a Java toolkit for creating arbitrary Artificial Neural Networks.
 * 
 * Copyright (c) 2009 Matthijs Snel
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.jann;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

import org.apache.commons.math.stat.descriptive.SummaryStatistics;


public class Trainer {

	/**
	 * Maps training vectors to target vectors
	 */
	protected List<DataItem> data, trainingData, testData;
	/**
	 * Network to be trained
	 */
	protected NeuralNet trainee;
	/**
	 * Sum of squared errors
	 */
	protected double[] SSE;
	/**
	 * Error for curr epoch
	 */
	protected double currEpochErr, trainingPercentage;
	protected int currItem;
	
	public Trainer(double trainingDataPercentage) {
		trainingData = new ArrayList<DataItem>();
		data = new ArrayList<DataItem>();
		testData = new ArrayList<DataItem>();
		currEpochErr = 0;
		currItem = 0;
		trainingPercentage = trainingDataPercentage;
	}
	
	public void addDataItem( DataItem item ) {
		data.add(item);
	}
	
	public void addData( Collection<? extends DataItem> data ) {
		this.data.addAll(data);
	}
	
	public void clearData() {
		data.clear();
	}
	
	public void printSSE() {
		System.out.println(SSE);
	}
	
	public void setTrainee( NeuralNet toTrain ) {
		trainee = toTrain;
	}
	
	public void train( int epochs ) {
		//System.out.println("TRAIN");
		SSE = new double[epochs];
		//separateTrainingData();
		//SSE[0] = doEpoch(trainingData, false); //initial SSE without learning
		for ( int i = 0; i < epochs; i++ )
			SSE[i] = doEpoch();
		//System.out.println(Arrays.toString(SSE));
	}
	
	public void separateTrainingData() {
		trainingData.clear();
		testData.clear();
		for ( DataItem item : data ) {
			if ( Math.random() < trainingPercentage )
				trainingData.add(item);
			else
				testData.add(item);
		}
	}
	
	public void normalizeData() {
		int len = data.get(0).input.length;
		SummaryStatistics stat[] = new SummaryStatistics[len];
		for ( int i = 0; i < len; i++ )
			stat[i] = new SummaryStatistics();
		for ( DataItem itm : data )
			for ( int i = 0; i < len; i++ )
				stat[i].addValue(itm.input[i]);
		for ( DataItem itm : data )
			for ( int i = 0; i < len; i++ )
				itm.input[i] = (itm.input[i] - stat[i].getMean()) / stat[i].getStandardDeviation();
	}
	
	public void showNext() {
		trainee.feedforward( trainingData.get(currItem).input );
		System.out.println( Arrays.toString(trainingData.get(currItem).input) + "->" + Arrays.toString(trainee.getOutput()) );
		if ( ++currItem >= trainingData.size() )
			currItem = 0;
	}
	
	protected double[] computeError( double[] o, double t[] ) {
		double delta[] = new double[o.length],
				error = 0;
		for ( int i = 0; i < delta.length; i++ ) {
			delta[i] = o[i] - t[i];
			error += delta[i] * delta[i];
		}
		//System.out.println("Delta: " + delta[0]);
		currEpochErr += error;
		return delta;
	}
	
	/**
	 * 
	 * @return SSE for this epoch
	 */
	public double doEpoch() {
		return doEpoch(trainingData, true);
	}
	
	public double doEpoch(List<DataItem> data, boolean train) {
		currEpochErr = 0;
		double[] err, output;
		for ( DataItem item : data ) {
			trainee.feedforward(item.input);
			output = trainee.getOutput();
			if ( Double.isNaN(output[0]) ) {
				throw new RuntimeException("Weights are infinity");
			}
			err = computeError(output, item.target);
			if ( train ) {
				trainee.backprop( err );
			}
			else {
				System.out.println(item + "," + Arrays.toString(output) + "," + Arrays.toString(err));
			}
		}
		return currEpochErr;
	}
	
	public double[] getTrainingSSE() {
		return SSE;
	}
	
	public double test() {
		//System.out.println("TEST");
		return doEpoch(testData, false);
	}
}
